/* Copyright (c) 2007 John Wells

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */

// Short-cuts
using EntityKeyValuePair = System.Collections.Generic.KeyValuePair<string, object>;

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Graphics;

using JohnWells.GameLevel;
using JohnWells.GameLevel.ContentPipeline.Leadwerks14;
//using JohnWells.GameLevel.ContentPipeline.Properties;
using JohnWells.GameLevel.ContentPipeline.Serialization;
using JohnWells.GameLevel.Vertices;
using NormalMappingEffectPipeline;

namespace JohnWells.GameLevel.ContentPipeline.Leadwerks14
{
    [ContentProcessor(DisplayName = "Level (.3dw) - John Wells")]
    internal class Leadwerks14ModelProcessor : ContentProcessor<Leadwerks14Content, ModelContent>
    {
        #region Fields
        // Private variables
        private Leadwerks14Content _content;
        private ContentProcessorContext _context;
        private LevelContent _output = new LevelContent();
        
        // These caches are kept to make processing quicker
        private SortedList<string, SurfaceDocument> _cachedSurfaces = new SortedList<string, SurfaceDocument>();
        private SortedList<int, SurfaceDocument> _vertexSurfaces = new SortedList<int, SurfaceDocument>();
        private Dictionary<BspNode, IEnumerable<Brush>> _nodeBrushes = new Dictionary<BspNode, IEnumerable<Brush>>();

        // Other unimportant private variables
        private Random _random = new Random();
        #endregion

        
        public override ModelContent Process(Leadwerks14Content input,
                                             ContentProcessorContext context)
        {
            // Note: Uncomment the next line to debug this processor
            //System.Diagnostics.Debugger.Launch();

            // Store these values for the process methods
            this._content = input;
            this._context = context;

            // These calls must happen in this order to process the .3dw file
            this.LoadSurfaces();
            this.BuildEntities();
            this.BuildHeadNode();
            this.BuildAmbientOcclusion();

            this._output.Lightmaps = this._content.Lightmaps;

            return this._output;
        }

        #region Ambient Occlusion Methods
        private void BuildAmbientOcclusion()
        {
           this._context.Logger.LogMessage("Building ambient occlusion");
                
            // Sample the ambient term for each brush vertex
            for (int index = 0; index < this._output.Vertices.Count; index++)
            {
                // Grab the vertex and the surface that it uses
                BrushVertex vertex = this._output.Vertices[index];
                SurfaceDocument surface = this._vertexSurfaces[index];

                // Sample and stick this vertex back in the buffer
                this.SampleAmbientOcclusion(ref vertex, surface);
                this._output.Vertices[index] = vertex;
            }
        }

        /// <summary>This method uses the pre-computed spatial partioning tree (head node) to calculate
        /// the ambient occlusion value of a given vertex. The surface document contains settings that
        /// describe how many rays we should sample and how far away to consider other surfaces as
        /// occluding this vertex.</summary>
        private void SampleAmbientOcclusion(ref BrushVertex vertex, SurfaceDocument surface)
        {
            // Don't calculate anything for surfaces that don't specify AO in their surface document
            SurfaceDocument.AmbientOcclusionElement ao = surface.AmbientOcclusion;
            if (ao == null)
                return;

            // This is the ray-sampler loop: 1 to N samples
            int hits = 0;
            for (int sample = 0; sample < ao.Samples; sample++)
            {
                // Generate a random vector the length of our distance value
                // (Better guidance than this bad code: http://mathworld.wolfram.com/SpherePointPicking.html)
                float x = 2f * (float)this._random.NextDouble() - 1f;
                float y = 2f * (float)this._random.NextDouble() - 1f;
                float z = 2f * (float)this._random.NextDouble() - 1f;
                Vector3 ray = new Vector3(x, y, z);
                ray = Vector3.Normalize(ray);
                ray = Vector3.Multiply(ray, ao.Distance);

                // Flip the vector if it's pointing into the face this vertex lies on
                // (This makes all AO samples hemispherical from the face of the vertex)
                if (Vector3.Dot(vertex.Normal, ray) < 0f)
                    ray = Vector3.Negate(ray);

                Vector3 start = vertex.Position;
                Vector3 end = Vector3.Add(start, ray);


                CollisionResult? result = this.CollideSegment(this._output.HeadNode, start, end);
                if (result.HasValue)
                    hits++;
            }
            
            // The ambient occlusion term ends up being the distance that the sample rays travelled
            // before hitting another face over the total distance they could have travelled.
            // (Low numbers are more occluded/darker than higher numbers)
            vertex.AmbientOcclusion = 1f - ((float)hits / ao.Samples);
        }
        #endregion

        #region Bsp Methods
        private void BuildHeadNode()
        {
            // What a boring level....
            if (this._content.Brushes == null)
                return;

            // Add all visible faces to a list
            List<BrushFace> faces = new List<BrushFace>();
            foreach (Brush brush in this._content.Brushes)
                foreach (BrushFace face in brush.VisibleFaces)
                {
                    // This is a final check to make sure we're playing with faces that have
                    // valid surface documents attached to them...
                    if (face.Surface == null)
                        continue;

                    faces.Add(face);
                }

            // Recursively widdle down that list making nodes...
            this._output.HeadNode = this.BuildNodeRecursive(faces);
        }

        private BspNode BuildNodeRecursive(IEnumerable<BrushFace> faces)
        {
            // Two sets of faces, one for the + side of the tree, and one for the - side
            List<BrushFace> frontFaces = new List<BrushFace>();
            List<BrushFace> backFaces = new List<BrushFace>();

            // If all faces form a convex set, then we're looking at a leaf node
            // (Note: I'm still playing with the rules, convex might be too pushy...)
            if (PartitioningHelper.IsLeafSet(faces))
                return this.BuildLeafNode(faces);

            BspNode node = new BspNode();

            // The dividing face is what will seperate the two halves of this BSP node
            node.DividingPlane = PartitioningHelper.ChooseDividingPlane(faces);

            // Build nodes...
            node.FrontChild = this.BuildNodeRecursive(PartitioningHelper.GetPostiveFaces(faces, node.DividingPlane));
            node.BackChild = this.BuildNodeRecursive(PartitioningHelper.GetNegativeFaces(faces, node.DividingPlane));
            
            return node;
        }

        private BspNode BuildLeafNode(IEnumerable<BrushFace> faces)
        {
            BspNode leaf = new BspNode();
            List<int> primitiveIndices = new List<int>();

            // Find the unique brushes that created these faces
            List<Brush> brushes = new List<Brush>();
            foreach (BrushFace face in faces)
                if (!brushes.Contains(face.Brush))
                    brushes.Add(face.Brush);
            
            // Associate the brushes used to create these faces with this node (for processing use)
            this._nodeBrushes.Add(leaf, brushes);

            // Collect all the vertices for this leaf into a sorted-by-material list
            Dictionary<SurfaceDocument, List<BrushFace>> sortedFaces = new Dictionary<SurfaceDocument, List<BrushFace>>();
            foreach (BrushFace face in faces)
            {
                // If we have not yet accumulated faces for this material, create a list for them
                if (!sortedFaces.ContainsKey(face.Surface))
                    sortedFaces.Add(face.Surface, new List<BrushFace>());

                sortedFaces[face.Surface].Add(face);
            }

            // Turn the sorted sets of faces into drawable triangle list primitives
            foreach (SurfaceDocument surface in sortedFaces.Keys)
            {
                // Collect all of the vertices for this face in one huge triangle list
                List<BrushVertex> vertices = new List<BrushVertex>();
                foreach (BrushFace face in sortedFaces[surface])
                {
                    // Brush faces are fans -> Convert to list style
                    BrushVertex[] fan = face.Vertices.ToArray();
                    BrushVertex[] list = TriangleHelper.ConvertFanToList(fan);

                    // It's safe to concatenate lists
                    vertices.AddRange(list);
                }

                //TODO: This should be much simpler/clearer
                // Keep track of the number of items in the vertex buffer before and after building the triangle list
                int startingIndex = this._output.Vertices.Count;
                primitiveIndices.Add(this._output.Primitives.Count);
                this._output.Primitives.Add(TriangleHelper.BuildTriangleList(surface, vertices, this._output.Vertices));
                int endingIndex = this._output.Vertices.Count;

                // If we ended up adding vertexes, keep track of which surfaces they used
                // (This comes in handy when doing per-vertex ambient occlusion calculations)
                if (endingIndex > startingIndex)
                    for (int index = startingIndex; index < endingIndex; index++)
                        this._vertexSurfaces.Add(index, surface);
            }

            // Set the indices to the recently added primitives
            leaf.PrimitiveIndices = primitiveIndices.ToArray();

            return leaf;
        }
        #endregion

        #region Entity Methods
        /// <summary>This method converts the 3dw style entities to Level-style.</summary>
        private void BuildEntities()
        {
            if (this._content.Entities != null)
                foreach (Entity entity in this._content.Entities)
                {
                    // Create the output variables that will be added to the results list
                    Level.Entity newEntity = new Level.Entity();
                    List<EntityKeyValuePair> items = new List<EntityKeyValuePair>();

                    // Position is unchanged
                    newEntity.Position = entity.Position;

                    // Copy items (optionally in the future change the type of the value object to reflect color, position, etc)
                    foreach (EntityKeyValuePair item in entity.Items)
                        items.Add(new EntityKeyValuePair(item.Key, item.Value));

                    // Assign the items and add it to the output list
                    newEntity.Items = items.ToArray();
                    this._output.Entities.Add(newEntity);
                }
        } 
        #endregion

        #region Surface Methods
        private void LoadSurfaces()
        {
            // Skip when we have no brushes - boring level
            if (this._content.Brushes == null)
                return;

            // Load all surfaces for all faces of all brushes
            foreach (Brush brush in this._content.Brushes)
                foreach (BrushFace face in brush.AllFaces)
                {
                    // Hidden faces don't have materials to load...
                    if (face.Material == null || string.IsNullOrEmpty(face.Material.Filename))
                        continue;

                    // Load the surface name using the materials' filename
                    string surfaceName = face.Material.Filename;
                    SurfaceDocument surface = this.LoadSurface(surfaceName);

                    // Set this so the remaining processing has handy access to it
                    face.Surface = surface;
                }
        }

        /// <summary>Caches the surface data and builds related textures and effects</summary>
        private SurfaceDocument LoadSurface(string surfaceName)
        {
            if (this._cachedSurfaces.ContainsKey(surfaceName))
                return this._cachedSurfaces[surfaceName];
            else
            {
                // All materials should be located in the same directory as the content being processed
                string path = Path.GetDirectoryName(this._content.Identity.SourceFilename);

                // The surface file is named after the material in the 3dw file
                string filename = Path.Combine(path, Path.ChangeExtension(surfaceName, "xml"));

                // Asset that this file exists
                if (!File.Exists(filename))
                    throw new InvalidContentException(string.Format("Surface file not found: {0}", filename), new ContentIdentity(filename));

                // Load the surface file (Push, Log, Load, Pop)
                this._context.Logger.PushFile(filename);
                this._context.Logger.LogMessage("Loading surface {0} ", filename);
                SurfaceDocument surface = SurfaceDocument.FromFile(Path.Combine(path, filename));
                this._context.AddDependency(filename);
                this._context.Logger.PopFile();

                // Build the effect asset
                string sourceEffect = Path.Combine(path, surface.Effect);
                this._context.AddDependency(sourceEffect);
                this.BuildAsset<EffectContent, CompiledEffect>(sourceEffect, typeof(EffectProcessor).Name, surface.AssetName);

                // Build the texture assets
                //System.Diagnostics.Debugger.Launch();
                foreach (SurfaceDocument.TextureInstanceElement instance in surface.TextureInstances)
                {
                    // Build this texture asset
                    string sourceTexture = Path.Combine(path, instance.Filename);
                    this._context.AddDependency(sourceTexture);
                    if (instance.Parameter.Contains("NormalTexture"))
                    {
                        this.BuildAsset<TextureContent, TextureContent>(sourceTexture, typeof(ModelMapTextureProcessor).Name, instance.AssetName);
                    }
                    else
                    {
                        this.BuildAsset<TextureContent, TextureContent>(sourceTexture, typeof(TextureProcessor).Name, instance.AssetName);
                    }
                }

                // Keep the loaded material data in the cache
                this._cachedSurfaces.Add(surfaceName, surface);

                return surface;
            }
        }

        private void BuildAsset<T, K>(string filename, string processorName, string assetName)
        {
            this._context.Logger.PushFile(filename);

            // Build and process the effect asset
            ExternalReference<T> sourceAsset = new ExternalReference<T>(filename);
            try
            {
                ExternalReference<K> builtAsset = this._context.BuildAsset<T, K>(sourceAsset, processorName, null, null, assetName);
                
                // Add an external dependency to this file
                this._context.AddDependency(builtAsset.Filename);
                this._context.Logger.PopFile();
            }
            catch (PipelineException ex)
            {
                // XNA Does this crazy thing where any existing XNB file disallows other XNB files of the same name
                // from being built - Why oh why could it not check the existing XNB and say, oh look - This is the
                // same asset - I guess I'll just accept that I've already done this. Or hey, it could also just
                // do what the developer says! If I say THIS IS THE ASSET NAME, gosh darn it believe me! If that
                // name already exists, well for holy sake just give me the benefit of the doubt that I know what
                // I'm building! If nothing else, don't fail on me without giving me a) a way to see if this will
                // fail before trying it or b) A specific exception so I don't have to do the crappy locale-specific
                // message-text checking that I'm doing below. Otherwise, I love this whole XNA thing. Thanks!
                if (ex.Message == string.Format("More than one output asset is named \"{0}\".\n You must explicitly specify asset names to resolve the conflict.", assetName))
                    // Do nothing - this exception can safely be ignored because I know what I'm doing
                    this._context.Logger.PopFile();
                else
                    throw;
            }
        }
        #endregion

        #region Collision Methods
        #region Nested Struct: TraceResult
        public struct TraceResult
        {
            #region Fields
            // Private variables
            private readonly float _ratio;
            private readonly Plane _collisionPlane;
            #endregion

            public TraceResult(float ratio, Plane collisionPlane)
            {
                // Sanity checks
                if (ratio < 0.0f || ratio > 1.0f)
                    throw new ArgumentOutOfRangeException("ratio");
                if (collisionPlane.Normal == Vector3.Zero)
                    throw new ArgumentOutOfRangeException("collisionPlane");

                // Set the values
                this._ratio = ratio;
                this._collisionPlane = collisionPlane;
            }

            #region Properties
            public float Ratio
            {
                get { return this._ratio; }
            }

            public Plane CollisionPlane
            {
                get { return this._collisionPlane; }
            }
            #endregion
        }
        #endregion

        #region Nested Struct: CollisionResult
        public struct CollisionResult
        {
            #region Fields
            // Private variables held as input
            private readonly Vector3 _start, _end;
            private readonly TraceResult _traceResult;

            // Private variables that are calculated on-demand
            private Vector3? _position;
            private float? _distanceSquared;
            private float? _distance;
            #endregion

            public CollisionResult(Vector3 start, Vector3 end, TraceResult traceResult)
            {
                // Set the required values
                this._start = start;
                this._end = end;
                this._traceResult = traceResult;

                // Set these values to null so they get lazy-calculated as needed
                this._position = null;
                this._distanceSquared = null;
                this._distance = null;
            }

            #region Properties
            public Plane CollisionPlane
            {
                get { return this._traceResult.CollisionPlane; }
            }

            public Vector3 Position
            {
                get
                {
                    // Lazy calculate this value
                    if (!this._position.HasValue)
                        this._position = Vector3.Lerp(this._start, this._end, this._traceResult.Ratio);

                    // Return the point of collision
                    return (Vector3)this._position;
                }
            }

            public float DistanceSquared
            {
                get
                {
                    // Lazy calculate this value
                    if (!this._distanceSquared.HasValue)
                        this._distanceSquared = Vector3.DistanceSquared(this._start, this.Position);

                    // Return the distance squared
                    return (float)this._distanceSquared;
                }
            }

            public float Distance
            {
                get
                {
                    // Lazy calculate this value
                    if (!this._distance.HasValue)
                        this._distance = Vector3.Distance(this._start, this.Position);

                    // Return the distance
                    return (float)this._distance;
                }
            }
            #endregion
        }
        #endregion

        private CollisionResult? CollideSegment(BspNode node, Vector3 start, Vector3 end)
        {
            // Use the sphere method, but specify a no-size sphere (a point)
            return CollideSphere(node, start, end, 0.0f);
        }

        private CollisionResult? CollideSphere(BspNode node, Vector3 start, Vector3 end, float radius)
        {
            // Sanity checks
            if (radius < 0.0f)
                throw new ArgumentOutOfRangeException("radius");

            // Trace it a-la Quake style (but in C#)
            TraceResult? result = this.TraceNode(node, start, end, radius);

            // Nothing collided? Return null.
            if (result == null)
                return null;

            // Done!
            return new CollisionResult(start, end, result.Value);
        }

        /// <summary>The value returned by this function is in the range of 0..1 and represents how far along the
        /// given line segment we were able to travel without colliding into the visible portion of any brushes brushes.</summary>
        private TraceResult? TraceNode(BspNode node, Vector3 start, Vector3 end, float offset)
        {
            // Get the brushes for this node
            IEnumerable<Brush> brushes = this._nodeBrushes[node];

            // If this node is a leaf node we should check each brush
            if (brushes != null)
            {
                // Find the lowest ratio collision for this cluster
                // (Lowest ratio means closest to the start vector)
                TraceResult? firstCollision = null;
                foreach (Brush brush in brushes)
                {
                    TraceResult? result = Leadwerks14LevelProcessor.TraceBrush(brush, start, end, offset);

                    // If we have not yet found a result OR we have and this result
                    // is closer, update our 'firstCollision' variable
                    if (!firstCollision.HasValue || (result.HasValue && result.Value.Ratio < firstCollision.Value.Ratio))
                        firstCollision = result;
                }

                // Return what we found, which might be null
                return firstCollision;
            }

            // This is not a leaf node - so we need to search it further
            // Figure out how far in front of or behind these vectors are compared to our dividing plane
            float startDistance = PlaneExtensions.Distance(node.DividingPlane, start);
            float endDistance = PlaneExtensions.Distance(node.DividingPlane, end);

            // Classify where these vectors sit compared to our dividing plane
            if (startDistance >= offset && endDistance >= offset)
                // Both points are in front of our dividing plane - only check front child
                return this.TraceNode(node.FrontChild, start, end, offset);
            else if (startDistance < -offset && endDistance < -offset)
                // Both points are behind our dividing plane - only check back child
                return this.TraceNode(node.BackChild, start, end, offset);
            else
            {
                // Try the 'front' side of the node
                TraceResult? frontResult = this.TraceNode(node.FrontChild, start, end, offset);

                // Try the 'back' side of the node
                TraceResult? backResult = this.TraceNode(node.BackChild, start, end, offset);

                // Test both sides and figure out which we should return
                if (!frontResult.HasValue && !backResult.HasValue)
                    return null;
                else if (frontResult.HasValue && !backResult.HasValue)
                    return frontResult;
                else if (!frontResult.HasValue && backResult.HasValue)
                    return backResult;
                else if (frontResult.Value.Ratio < backResult.Value.Ratio)
                    return frontResult;
                else
                    return backResult;
            }
        }

        /// <summary>Given a start and end position with a capsule shaped radius, determine if or where a given
        /// line segment make contact with a given brush</summary>
        private static TraceResult? TraceBrush(Brush brush, Vector3 start, Vector3 end, float radius)
        {
            // Start off with no collision (0.0f, 1.0f) and try to find something better
            float startFraction = 0.0f, endFraction = 1.0f;
            BrushFace collisionFace = null;

            foreach (BrushFace face in brush.AllFaces)
            {
                // Compute the distances from the ends of this line segment to the plane
                float startDistance = PlaneExtensions.Distance(face.Plane, start) - radius;
                float endDistance = PlaneExtensions.Distance(face.Plane, end) - radius;

                // Skip brushes where both points are in front of any one plane
                // Visualize the convex geometry of a brush to understand this.
                if (startDistance > 0.0f && endDistance > 0.0f)
                    return null;

                // Skip planes where both points are behind the plane - has no effect
                // on the collision
                if (startDistance <= 0.0f && endDistance <= 0.0f)
                    continue;

                // Classify this line segment
                if (startDistance > endDistance)
                {
                    // This line segment is entering the face
                    float fraction = startDistance / (startDistance - endDistance);

                    // Did we hit a better face?
                    if (fraction > startFraction)
                    {
                        // Mark this as the face we collided with
                        startFraction = fraction;
                        collisionFace = face;
                    }
                }
                else
                    // This line segment is leaving the face
                    endFraction = Math.Min(endFraction, startDistance / (startDistance - endDistance));
            }

            // If startFraction is > endFraction then the line segment never crossed into the
            // center of brush, return null (no collision)
            if (collisionFace == null || startFraction > endFraction)
                return null;
            else if ((collisionFace.Flags & BrushFace.FaceFlags.Hidden) != BrushFace.FaceFlags.None)
                // We did hit this brush, but we hit an invisible face -> There is no chance we could therefore
                // enter the face from any other forward-facing angle so we'll just escape through the nearest backface
                return null;
            else
                // Otherwise return the fractional point in the line segment where we first hit solid
                return new TraceResult(startFraction, collisionFace.Plane);
        }
        #endregion
    }
}